package com.guohe3.just.craw;

import com.guohe3.just.DO.Score;
import com.guohe3.just.DO.Student;
import com.guohe3.just.common.constants.Constants;
import com.guohe3.just.common.enums.CrawTypeEnum;
import com.guohe3.just.common.enums.ResultEnum;
import com.guohe3.just.common.execption.CustomException;
import com.guohe3.just.common.utils.SecurityUtils;
import com.guohe3.just.config.SSLSocketClient;
import com.guohe3.just.mapper.StudentMapper;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.StringUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.text.DecimalFormat;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @author 浦希成
 * 2019-01-09
 */
@Service
@Slf4j
public class CrawServiceImpl implements CrawService {

    private static List<Map<String, String>> vpnAccountList = Collections.synchronizedList(Arrays.asList(
            new ConcurrentHashMap<String, String>() {
                {
                    put("username", "152210702119");
                    put("password", "935377012");
                }
            },
            new ConcurrentHashMap<String, String>() {
                {
                    put("username", "152210702119");
                    put("password", "935377012");
                }
            },
            new ConcurrentHashMap<String, String>() {
                {
                    put("username", "152210702119");
                    put("password", "935377012");
                }
            },
            new ConcurrentHashMap<String, String>() {
                {
                    put("username", "152210702119");
                    put("password", "935377012");
                }
            },
            new ConcurrentHashMap<String, String>() {
                {
                    put("username", "152210702119");
                    put("password", "935377012");
                }
            }

    ));


    @Autowired
    private StudentMapper mapper;

    public static String getSourceHtml(OkHttpClient client, String url) throws IOException {
        Request request = new Request.Builder()
                .url(url)
                .build();
        Response response = client.newCall(request).execute();
        if (response.isSuccessful()) {

            return response.body().string();
        } else {
            throw new CustomException(ResultEnum.REQUEST_ERROR);
        }

    }

    @Override
    public OkHttpClient login(String username, String password, CrawTypeEnum typeEnum) throws IOException {
        if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
            throw new CustomException(ResultEnum.OBJECT_NULL_ERROR);
        }
        String loginJWTLURl;
        if (typeEnum == CrawTypeEnum.NORMAL) {
            loginJWTLURl = Constants.NORMAL_LOGIN_JWGL;
        } else {
            loginJWTLURl = Constants.VPN_LOGIN_JWGL;
        }
        OkHttpClient client;
        //判断爬虫方式
        if (typeEnum == CrawTypeEnum.NORMAL) {
            client = getNormalClient();
        } else {
            client = getVpnClient();
            FormBody.Builder params = new FormBody.Builder();
            Map<String, String> map = vpnAccountList.get(RandomUtils.nextInt(1, 5));
            String vpnUsername = map.get("username");
            String vpnPassword = map.get("password");
            params.add("USERNAME", vpnUsername);
            params.add("PASSWORD", vpnPassword);
            params.add("realm", "LDAP-REALM");
            params.add("btnSubmit", "登录");
            params.add("tz_offset", "480");

            Request request = new Request.Builder()
                    .post(params.build())
                    .url(Constants.LOGIN_TO_VPN)
                    .build();
            Response response = client.newCall(request).execute();

            String result = response.body().string();
            if (!response.isSuccessful()) {
                throw new CustomException(ResultEnum.LOGIN_VPN_FAIL);
            } else {
                //vpn账号已登录
                if (result.contains("DSIDFormDataStr")) {
                    Document doc = Jsoup.parse(result);
                    String formDataStr = doc.getElementById("DSIDFormDataStr").val();
                    FormBody.Builder params2 = new FormBody.Builder();

                    params2.add("btnContinue", "继续会话");
                    params2.add("FormDataStr", formDataStr);
                    Request request2 = new Request.Builder()
                            .post(params2.build())
                            .url(Constants.LOGIN_TO_VPN)
                            .build();
                    Response response2 = client.newCall(request2).execute();
                    if (!response2.isSuccessful()) {
                        throw new CustomException(ResultEnum.LOGIN_VPN_FAIL);
                    } else {
                        log.info("vpn登录成功:时间：{}", LocalDateTime.now().toString());
                    }
                    //账号或者密码错误
                } else if (result.contains("江苏科技大学VPN服务")) {
                    throw new CustomException(ResultEnum.LOGIN_VPN_FAIL);
                } else {
                    log.info("vpn登录成功:用户名：{}，密码：{}，时间：{}", vpnUsername, vpnPassword, LocalDateTime.now().toString());
                }
            }
        }
        FormBody.Builder params = new FormBody.Builder();
        params.add("USERNAME", username);
        params.add("PASSWORD", password);
        Request request = new Request.Builder()
                .post(params.build())
                .url(loginJWTLURl)
                .build();
        try {
            Response response = client.newCall(request).execute();
            if (response.isSuccessful()) {

                String result = response.body().string();
                //login success
                if (result.contains("我的桌面")) {
                    log.info("教务系统登录成功，用户名：{}，密码：{}，时间：{}", username, password, LocalDateTime.now().toString());
                    return client;
                } else {
                    log.error("登录失败，用户名：{}，密码：{}，时间：{}", username, password, LocalDateTime.now().toString());
                    throw new CustomException(ResultEnum.JWC_ACCOUNT_ERROR);
                }
            } else {
                log.info("教务处异常：{}", response.message());
                return null;
            }
        } catch (SocketTimeoutException | UnknownHostException e) {
            log.info("教务处异常：{}", e);
            return null;
        }

    }

    @Override
    public List<Score> crawScoreAll(String username, String password, CrawTypeEnum typeEnum) throws IOException {
        String scoreUrl;
        if (typeEnum == CrawTypeEnum.NORMAL) {
            scoreUrl = Constants.NORMAL_SCORE_URL;
        } else {
            scoreUrl = Constants.VPN_SCORE_URL;
        }
        OkHttpClient client = login(username, password, typeEnum);
        if (client == null) {
            throw new CustomException(ResultEnum.LOGIN_FAIL);
        }
        String result = getSourceHtml(client, scoreUrl);
        Document doc = Jsoup.parse(result);
        Element table = doc.getElementById("dataList");
        if (table == null) {
            log.info("用户 {} 未评价", username);
            throw new CustomException(ResultEnum.NOT_EVALUATED);
        }
        Elements trs = table.select("tr");
        trs.remove(0);
        return trs.stream()
                .map(tr -> {
                    Score score = new Score();
                    score.setOrderNum(tr.child(0).text());
                    score.setStartSemester(tr.child(1).text());
                    score.setCourseNum(tr.child(2).text());
                    score.setCourseName(tr.child(3).text());
                    score.setScore(tr.child(4).text());
                    score.setCredit(tr.child(5).text());
                    score.setTotalHours(tr.child(6).text());
                    score.setExaminationMethod(tr.child(7).text());
                    score.setCourseAttribute(tr.child(8).text());
                    score.setCourseNature(tr.child(9).text());
                    score.setAlternativeCourseNumber(tr.child(10).text());
                    score.setAlternativeCourseName(tr.child(11).text());
                    score.setMarkOfScore(tr.child(12).text());
                    return score;
                }).collect(Collectors.toList());
    }

    @Override
    public Student crawStudentInfo(String username, String password, CrawTypeEnum typeEnum) throws Exception {
        String studentInfoUrl;
        if (typeEnum == CrawTypeEnum.NORMAL) {
            studentInfoUrl = Constants.NORMAL_STUDENT_INFO_URL;
        } else {
            studentInfoUrl = Constants.VPN_STUDENT_INFO_URL;
        }

        OkHttpClient client = login(username, password, typeEnum);
        if (client == null) {
            throw new CustomException(ResultEnum.LOGIN_FAIL);
        }
        String html = getSourceHtml(client, studentInfoUrl);
        Document doc = Jsoup.parse(html);
        Elements trs = doc.getElementById("xjkpTable").select("tr");
        String name = StringUtils.trim(trs.get(3).select("td").get(1).text());
        String birthday = StringUtils.trim(trs.get(4).select("td").get(1).text());
        String academy = trs.get(2).select("td").get(0).text().split("：")[1];
        String major = trs.get(2).select("td").get(1).text().split("：")[1];
        String classNum = trs.get(2).select("td").get(3).text().split("：")[1];
        String identityCard = trs.get(47).select("td").get(3).text();
        String dateOfEnrollment = trs.get(46).select("td").get(1).text();

        String sex = StringUtils.trim(trs.get(3).select("td").get(3).text());
        Student student = new Student();
        student.setSex(sex);
        student.setName(name);
        student.setBirthday(birthday);
        student.setMajor(major);
        student.setAcademy(academy);
        student.setClassNum(classNum);
        student.setUsername(username);
        student.setPassword(SecurityUtils.encryption(password, Constants.SECURITY_KEY));
        student.setIdentityCard(identityCard);
       student.setDateOfEnrollment(dateOfEnrollment);
        Student studentDB = mapper.selectByUsernameAndPassword(username, password);
        if (studentDB==null){
            mapper.insert(student);
            log.info("保存用户数据：{}", student);
        }else {
            studentDB.setPassword(password);
            studentDB.setSex(sex);
            mapper.updateById(studentDB);
            log.info("用户修改密码，更新用户数据：{}", student);
        }
        return student;

    }

    @Override
    public List<Map<String, String>> crawSchoolTimetable(String username, String password, String semester, CrawTypeEnum typeEnum) throws IOException {

        String[] weekList = new String[]{
                "monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"
        };
        String kbUrl;
        if (typeEnum == CrawTypeEnum.NORMAL) {
            kbUrl = Constants.NORMAL_KB_URL;
        } else {
            kbUrl = Constants.VPN_KB_URL;
        }
        OkHttpClient client = login(username, password, typeEnum);
        if (client == null) {
            throw new CustomException(ResultEnum.LOGIN_FAIL);
        }
        String html = getSourceHtml(client, kbUrl + "?xnxq01id=" + semester);
        Document doc = Jsoup.parse(html);
        Elements trs = doc.select("#kbtable tr");
        trs.remove(0);
        List<Map<String, String>> result = new ArrayList<>(6);
        for (Element tr : trs) {

            Elements tds = tr.select(".kbcontent");
            int i = 0;
            Map<String, String> map = new HashMap<>(7);
            for (Element td : tds) {

                StringBuilder courseAll = new StringBuilder();
                for (String courseString : td.html().split("---------------------")) {
                    if (courseString.length() > 10) {
                        String course = courseString.replaceAll("<br>|<font title=\"老师\">|<font title=\"教室\">|<font title=\"周次\\(节次\\)\">|<\\/font>", "")
                                .replaceAll("(?m)^\\s*$" + System.lineSeparator(), "")
                                .replaceAll("[\r\n]", "@");
                        courseAll.append(course).append("---------------------");
                    }

                }
                int index = courseAll.lastIndexOf("---------------------");
                if (index != -1) {
                    String courseStrTemp = courseAll.toString().substring(0, index);
                    int indexTemp = courseStrTemp.lastIndexOf("@");
                    int lengthTemp = courseStrTemp.length();
                    String courseStr = courseStrTemp.substring(0, indexTemp) +
                            courseStrTemp.substring(indexTemp + 1, lengthTemp);
                    map.put(weekList[i], courseStr);
                }

                i++;
            }
            result.add(map);
        }
        return result;
    }

    @Override
    public List<Map<String, String>> crawJidian(String username, String password, CrawTypeEnum typeEnum) throws IOException {

        //返回结果
        List<Map<String, String>> result = new ArrayList<>();

        //成绩集合
        List<Score> scoreList = crawScoreAll(username, password, typeEnum);

        //学期分组绩点
        List<List<Score>> oneSemesterList = new ArrayList<>();

        group(result, scoreList, oneSemesterList);
        result.sort(Comparator.comparing((Map<String, String> map) -> map.get("year")));

        //学年分组绩点
        List<List<Score>> twoSemesterlist = new ArrayList<>();
        for (Score score : scoreList) {
            score.setStartSemester(score.getStartSemester().substring(0, 9));
        }
        group(result, scoreList, twoSemesterlist);
        //总绩点
        double creditPointSumAll = scoreList.stream().mapToDouble(item -> Double.parseDouble(item.getCredit()) * getScoreNum(item.getScore())).sum();
        double creditSUmAll = scoreList.stream().mapToDouble(item -> Double.parseDouble(item.getCredit())).sum();
        Map<String, String> mapAll = new HashMap<>(2);
        mapAll.put("year", "all");
        mapAll.put("point", new DecimalFormat("#.##").format(creditPointSumAll / creditSUmAll) + "");
        result.add(0, mapAll);
        return result;
    }

    @Override
    public List<String> crawAllYearList(String username, String password, CrawTypeEnum typeEnum) throws Exception {
        String kbUrl;
        if (typeEnum == CrawTypeEnum.NORMAL) {
            kbUrl = Constants.NORMAL_KB_URL;
        } else {
            kbUrl = Constants.VPN_KB_URL;
        }
        Student studentInfo = crawStudentInfo(username, password, typeEnum);
        //得到学生的入学日期
        int dateOfEnrollment = Integer.parseInt(studentInfo.getDateOfEnrollment().split("-")[0]);
        OkHttpClient client = login(username, password, typeEnum);
        if (client == null) {
            return Arrays.asList("2017-2018-1", "2017-2018-2", "2018-2019-1", "2018-2019-2");
        }
        String html = getSourceHtml(client, kbUrl + "?xnxq01id=" + "");
        Document doc = Jsoup.parse(html);
        Elements trs = doc.select("#xnxq01id option");
        List<String> result = new ArrayList<>();
        for (Element element : trs) {
            if (Integer.parseInt(element.text().split("-")[0]) >= dateOfEnrollment) {
                result.add(element.text());
            }
        }
        return result;
    }

    private OkHttpClient getNormalClient() {
        return new OkHttpClient.Builder()
                .connectTimeout(5, TimeUnit.SECONDS)
                //管理cookie
                .cookieJar(new CookieJar() {
                    private final HashMap<String, List<Cookie>> cookieStore = new HashMap<>();

                    @Override
                    public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
                        cookieStore.put(url.host(), cookies);
                    }

                    @Override
                    public List<Cookie> loadForRequest(HttpUrl url) {
                        List<Cookie> cookies = cookieStore.get(url.host());
                        return cookies != null ? cookies : new ArrayList<>();
                    }
                })
                .build();
    }

    private OkHttpClient getVpnClient() throws IOException {
        OkHttpClient client = new OkHttpClient.Builder()
                .connectTimeout(10, TimeUnit.SECONDS)
                //管理cookie
                .cookieJar(new CookieJar() {
                    private final HashMap<String, List<Cookie>> cookieStore = new HashMap<>();

                    @Override
                    public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
                        List<Cookie> cookieList = cookieStore.get(url.host());
                        List cookiesArray = new ArrayList(cookies);
                        if (cookieList != null) {
                            cookiesArray.addAll(cookieList);
                        }
                        cookieStore.put(url.host(), cookiesArray);
                    }

                    @Override
                    public List<Cookie> loadForRequest(HttpUrl url) {
                        List<Cookie> cookies = cookieStore.get(url.host());
                        return cookies != null ? cookies : new ArrayList<>();
                    }
                })
                .sslSocketFactory(SSLSocketClient.getSSLSocketFactory())
                .hostnameVerifier(SSLSocketClient.getHostnameVerifier())
                .build();
        return client;


    }

    /**
     * @param result       最终返回结果
     * @param scoreList    总数据
     * @param semesterList 学年数据
     */
    private void group(List<Map<String, String>> result, List<Score> scoreList, List<List<Score>> semesterList) {
        scoreList.stream()
                //只计算必修科目
                .filter(item -> "必修".equalsIgnoreCase(item.getCourseAttribute()))
                //体育不算分
                .filter(item -> !item.getCourseName().contains("体育"))
                //按学年分组
                .collect(Collectors.groupingBy(Score::getStartSemester, Collectors.toList()))
                .forEach((name, scores) -> semesterList.add(scores));
        //分组求平均绩点 平均绩点的计算公式是 绩点*学分/学分之和
        for (List<Score> scores : semesterList) {
            Map<String, String> map = new HashMap<>(2);
            //学分绩点之和
            double creditPointSum = scores.stream()
                    .mapToDouble(item -> Double.parseDouble(item.getCredit()) * getScoreNum(item.getScore())).sum();
            //学分之和
            double creditSum = scores.stream()
                    .mapToDouble(item -> Double.parseDouble(item.getCredit())).sum();
            map.put("year", scores.get(0).getStartSemester());
            map.put("point", new DecimalFormat("#.##").format(creditPointSum / creditSum) + "");
            result.add(map);
        }
    }

    /**
     * 返回绩点
     *
     * @param score
     * @return
     */
    private double getScoreNum(String score) {
        if (StringUtils.isNumeric(score)) {
            double scoreNum = Double.parseDouble(score);
            return scoreNum >= 60 ? scoreNum / 10 - 5 : 0;
        } else if ("优".equals(score)) {
            return 4.5;
        } else if ("良".equals(score)) {
            return 3.5;
        } else if ("中".equals(score)) {
            return 2.5;
        } else if ("及格".equals(score)) {
            return 1.5;
        } else if ("不及格".equals(score)) {
            return 0;
        } else if ("通过".equals(score)) {
            return 2.5;
        } else if ("不通过".equals(score)) {
            return 0;
        } else {
            return 3;
        }
    }


}
